package org.meb.conquest.rest.service;

import java.io.Serializable;
import java.io.StringWriter;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.deltaspike.jpa.api.transaction.Transactional;
import org.meb.conquest.db.dao.JpaDao;
import org.meb.conquest.db.dao.Query;
import org.meb.conquest.db.model.Deck;
import org.meb.conquest.db.model.DeckMember;
import org.meb.conquest.db.model.loc.Card;
import org.meb.conquest.db.util.DatabaseUtils;
import org.meb.conquest.db.util.Transformers;
import org.meb.conquest.db.util.Utils;
import org.meb.conquest.rest.core.ExportType;
import org.meb.conquest.rest.core.ExportedDeck;
import org.meb.conquest.rest.exception.DeckException;
import org.w3c.dom.DOMConfiguration;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.bootstrap.DOMImplementationRegistry;
import org.w3c.dom.ls.DOMImplementationLS;
import org.w3c.dom.ls.LSOutput;
import org.w3c.dom.ls.LSSerializer;

public class DeckServiceImpl implements DeckService, Serializable {

	private static final long serialVersionUID = -3917684297662777248L;

	@Inject
	private EntityManager em;

	@Inject
	private CardService cardService;

	@Inject
	private QueryContext queryContext;

	@Override
	public Deck findUserDeck(Long id) {
		if (queryContext.getUserId() == null) {
			throw new IllegalStateException("User id is null");
		}

		DatabaseUtils.executeSetUserLang(em, queryContext.getUserLanguage());
		Deck deck = new Deck();
		deck.setId(id);
		deck.setUserId(queryContext.getUserId());
		return new JpaDao<Deck>(em).findUnique(deck);
	}

	@Override
	public List<Deck> findUserDecks() {
		return findUserDecks(new Query<>(new Deck()));
	}

	@Override
	public List<Deck> findUserDecks(Query<Deck> query) {
		if (queryContext.getUserId() == null) {
			throw new IllegalStateException("User id is null");
		}

		DatabaseUtils.executeSetUserLang(em, queryContext.getUserLanguage());

		Long oldUserId = query.getExample().getUserId();
		try {
			query.getExample().setUserId(queryContext.getUserId());
			return new JpaDao<Deck>(em).find(query);
		} finally {
			query.getExample().setUserId(oldUserId);
		}
	}

	@Override
	@Transactional
	public Deck saveUserDeck(Long id, Deck deck) throws DeckException {
		if (queryContext.getUserId() == null) {
			throw new IllegalStateException("User id is null");
		}

		DatabaseUtils.executeSetUserLang(em, queryContext.getUserLanguage());
		try {
			// check ids match
			if (id != null && !id.equals(deck.getId())) {
				throw new DeckException("error.deck.idMismatch");
			}

			// remove cards with null or zero quantity
			Iterator<DeckMember> memberIter = deck.getDeckMembers().iterator();
			while (memberIter.hasNext()) {
				DeckMember member = memberIter.next();
				Integer quantity = member.getQuantity();
				if (quantity == null || quantity.equals(0)) {
					memberIter.remove();
				}
			}

			if (id == null) {
				deck.setWarlord(cardService.findUnique(deck.getWarlord()));
				for (DeckMember member : deck.getDeckMembers()) {
					member.setDeck(deck);
					member.setCard(cardService.findUnique(member.getCard()));
				}
			} else {
				Deck persistent = findUserDeck(id);
				persistent.setName(deck.getName());
				persistent.setDescription(deck.getDescription());
				persistent.setConfigCsQuantity(deck.getConfigCsQuantity());

				// check warlord ids match
				if (!persistent.getWarlord().getId().equals(deck.getWarlord().getId())) {
					throw new DeckException("error.deck.warlordIdMismatch");
				}

				HashMap<Long, DeckMember> membersMap = new HashMap<>();
				MapUtils.populateMap(membersMap, deck.getDeckMembers(), Transformers.DEME_CARD_ID);
				HashMap<Long, DeckMember> persistentMembersMap = new HashMap<>();
				MapUtils.populateMap(persistentMembersMap, persistent.getDeckMembers(), Transformers.DEME_CARD_ID);

				// merge
				Iterator<DeckMember> iter = persistent.getDeckMembers().iterator();
				while (iter.hasNext()) {
					DeckMember persistentMember = iter.next();
					Long cardId = persistentMember.getCard().getId();
					if (!membersMap.containsKey(cardId)) {
						persistentMember.setDeck(null);
						iter.remove();
					}
				}

				for (DeckMember member : deck.getDeckMembers()) {
					Long cardId = member.getCard().getId();
					if (persistentMembersMap.containsKey(cardId)) {
						DeckMember persistentMember = persistentMembersMap.get(cardId);
						Integer quantity = member.getQuantity();
						if (quantity == null || quantity.equals(0)) {
							persistent.getDeckMembers().remove(persistentMember);
						} else {
							persistentMember.setQuantity(quantity);
						}
					} else {
						member.setDeck(persistent);
						member.setCard(cardService.findUnique(new Card(cardId)));
						persistent.getDeckMembers().add(member);
					}
				}
				deck = persistent;
			}

			Date date = new Date();
			deck.setUserId(queryContext.getUserId());
			deck.setModifyDate(date);
			if (deck.getId() == null) {
				deck.setCreateDate(date);
				em.persist(deck);
			} else {
				deck = em.merge(deck);
			}

			em.flush();
		} catch (DeckException | RuntimeException e) {
			if (e instanceof DeckException) {
				throw e;
			} else {
				throw new DeckException("error.deck.unknown", e);
			}
		}
		return deck;
	}

	@Override
	@Transactional
	public void deleteUserDeck(Long id) throws DeckException {
		if (queryContext.getUserId() == null) {
			throw new IllegalStateException("User id is null");
		}
		DatabaseUtils.executeSetUserLang(em, queryContext.getUserLanguage());
		Deck deck = findUserDeck(id);
		if (deck == null) {
			throw new DeckException("error.deck.missing");
		}
		em.remove(deck);
		em.flush();
	}

	@Override
	public ExportedDeck exportUserDeck(Long id, ExportType type) throws DeckException {
		if (queryContext.getUserId() == null) {
			throw new IllegalStateException("User id is null");
		}

		if (type == ExportType.OCTGN) {
			DatabaseUtils.executeSetUserLang(em, "en");
		} else {
			DatabaseUtils.executeSetUserLang(em, queryContext.getUserLanguage());
		}

		Deck deck = findUserDeck(id);
		if (deck == null) {
			throw new DeckException("error.deck.missing");
		}

		ExportedDeck exported;
		switch (type) {
			case OCTGN:
				exported = exportOctgn(deck);
				break;
			default:
				throw new IllegalArgumentException("Unsupported export type");
		}
		return exported;
	}

	private ExportedDeck exportOctgn(Deck deck) throws DeckException {
		String gameId = "af04f855-58c4-4db3-a191-45fe33381679";

		ExportedDeck exported = new ExportedDeck();
		exported.setName(deck.getName());
		exported.setTechName(Utils.toTechName(deck.getName()));

		try {
			DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
			Document doc = db.newDocument();

			Element deckElem = (Element) doc.appendChild(doc.createElement("deck"));
			deckElem.setAttribute("game", gameId);

			Card warlord = deck.getWarlord();
			Element warlordSectionElem = (Element) deckElem.appendChild(doc.createElement("section"));
			warlordSectionElem.setAttribute("name", "Warlord");
			warlordSectionElem.appendChild(createCardElement(doc, warlord.getOctgnId(), warlord.getName(), 1));

			Element armiesSectionElem = (Element) deckElem.appendChild(doc.createElement("section"));
			armiesSectionElem.setAttribute("name", "Armies");
			for (DeckMember member : deck.getDeckMembers()) {
				Card card = member.getCard();
				armiesSectionElem.appendChild(createCardElement(doc, card.getOctgnId(), card.getName(),
						member.getQuantity()));
			}

			exported.setData(serialize(doc));
		} catch (Exception e) {
			throw new DeckException("error.deck.export", e);
		}

		return exported;
	}

	private Element createCardElement(Document doc, String id, String name, Integer quantity) {
		Element cardElem = doc.createElement("card");
		cardElem.setAttribute("qty", quantity.toString());
		cardElem.setAttribute("id", id);
		if (StringUtils.isNotBlank(name)) {
			cardElem.appendChild(doc.createTextNode(name));
		}
		return cardElem;
	}

	private String serialize(Document doc) throws Exception {
		StringWriter writer = new StringWriter();

		DOMImplementationRegistry reg = DOMImplementationRegistry.newInstance();
		DOMImplementationLS impl = (DOMImplementationLS) reg.getDOMImplementation("LS");
		LSSerializer serializer = impl.createLSSerializer();
		serializer.getDomConfig().setParameter("format-pretty-print", true);
		LSOutput lso = impl.createLSOutput();
		lso.setCharacterStream(writer);
		serializer.write(doc, lso);

		return writer.toString();
	}
}